/*
 * Copyright 2010 Alexander Ulizko
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.ulizko.template.soy.utils;

import com.ulizko.template.soy.javaSrc.MethodDescription;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;

import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * General-purpose string utils.
 */
public class StringUtils {
    /**
     * Used at {@link #isValidPackageName}. List of reserved Java words.
     */
    private static final ImmutableList<String> listOfReservedWords = ImmutableList.of("abstract", "boolean", "break",
            "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else",
            "extends", "false", "final", "finally", "float", "for", "goto", "if", "implements", "import",
            "instanceof", "int", "interface", "long", "native", "new", "null", "package", "private", "protected",
            "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw",
            "throws", "transient", "true", "try", "void", "volatile", "while");

    private static final String javaDocStart = "\n     * This method has been automatically generated from ";
    private static final String javaDocEnd = "     * @param data SoyMapData object containing template parameters.\n" +
            "     * Example: \n" +
            "     * new SoyMapData(\"firstParameterName\", \"firstParameterValue\",\n" +
            "     *        \"secondParameterName\", \"secondParameterValue\", ...)\n" +
            "     * @param sb If this argument is passed, method will output null, \n" +
            "     * and append result to the supplied StringBuffer.\n" +
            "     * Using StringBuffer improves perfomance.\n" +
            "     * @return Result of template processing or null if StringBuffer was passed as a second parameter.\n     ";
    /**
     * Pattern for an all-upper-case word in a file name or identifier.
     */
    private static final Pattern ALL_UPPER_WORD =
            Pattern.compile("(?<= [^A-Za-z] | ^)  [A-Z]+  (?= [^A-Za-z] | $)", Pattern.COMMENTS);

    /**
     * Pattern for an all-lower-case word in a file name or identifier.
     */
    // Note: Char after an all-lower word can be an upper letter (e.g. first word of camel case).
    private static final Pattern ALL_LOWER_WORD =
            Pattern.compile("(?<= [^A-Za-z] | ^)  [a-z]+  (?= [^a-z] | $)", Pattern.COMMENTS);

    /**
     * Pattern for a character that's not a letter nor a digit.
     */
    private static final Pattern NON_LETTER_DIGIT = Pattern.compile("[^A-Za-z0-9]");


    /**
     * Generates valid java class from a provided string
     * i.e. AaaBbb or aaa_bbb --> AaaBbb.
     * Removes .soy suffix if present.
     * <p/>
     * Again, all code thankfully borrowed from Google Closure Template
     * (this time, from {@link com.google.template.soy.parseinfo.passes.GenerateParseInfoVisitor}).
     *
     * @param fileName File name.
     * @return The generated base Java class name (without any suffixes).
     */
    public static String generateBaseClassName(String fileName) {
        Preconditions.checkNotNull(fileName, "Trying to generate Java class name based " +
                "on Soy file name, but Soy file name was not provided.");

        if (fileName.toLowerCase().endsWith(".soy")) {
            fileName = fileName.substring(0, fileName.length() - 4);
        }
        return makeUpperCamelCase(fileName);
    }


    /**
     * Creates the upper camel case version of the given string (can be file name or identifier).
     *
     * @param str The string to turn into upper camel case.
     * @return The upper camel case version of the string.
     */
    private static String makeUpperCamelCase(String str) {

        str = makeWordsCapitalized(str, ALL_UPPER_WORD);
        str = makeWordsCapitalized(str, ALL_LOWER_WORD);
        str = NON_LETTER_DIGIT.matcher(str).replaceAll("");
        return str;
    }


    /**
     * Makes all the words in the given string into capitalized format (first letter capital, rest
     * lower case). Words are defined by the given regex pattern.
     *
     * @param str         The string to process.
     * @param wordPattern The regex pattern for matching a word.
     * @return The resulting string with all words in capitalized format.
     */
    private static String makeWordsCapitalized(String str, Pattern wordPattern) {

        StringBuffer sb = new StringBuffer();

        Matcher wordMatcher = wordPattern.matcher(str);
        while (wordMatcher.find()) {
            String oldWord = wordMatcher.group();
            StringBuilder newWord = new StringBuilder();
            for (int i = 0, n = oldWord.length(); i < n; i++) {
                if (i == 0) {
                    newWord.append(Character.toUpperCase(oldWord.charAt(i)));
                } else {
                    newWord.append(Character.toLowerCase(oldWord.charAt(i)));
                }
            }
            wordMatcher.appendReplacement(sb, Matcher.quoteReplacement(newWord.toString()));
        }
        wordMatcher.appendTail(sb);

        return sb.toString();
    }

    /**
     * Retreives soy fileName from generated comment line.
     * I.e. returns simple.soy for
     * "The functions below were generated from simple.soy."
     *
     * @param commentContent Content of the comment
     * @return soy file fileName
     */
    public static String retreiveSoyFileNameFromComment(String commentContent) {
        String soyFileName = "";

        Pattern pattern = Pattern.compile("were generated from ([a-zA-Z0-9\\._]+)\\.", Pattern.MULTILINE);
        Matcher matcher = pattern.matcher(commentContent);

        if (matcher.find()) {
            soyFileName = matcher.group(1);
        }

        return soyFileName;
    }

    /**
     * Generates method description {@link com.ulizko.template.soy.javaSrc.MethodDescription} and fills it
     * with method name, java package and java class
     *
     * @param methodName Name of the generated java method, like ru$cherinfo$template$pages$generalPage
     * @return {@link com.ulizko.template.soy.javaSrc.MethodDescription} instance
     */
    public static MethodDescription analyzeMethodName(String methodName) {
        MethodDescription result = new MethodDescription();
        ImmutableList<String> chunks = ImmutableList.copyOf(Splitter.on("$").split(methodName));

        // the last one chunk is the new method name
        result.setMethodName(chunks.get(chunks.size() - 1));
        // second to last chunk is java class name
        result.setClassName(StringUtils.generateBaseClassName(chunks.get(chunks.size() - 2)));
        // first chunks except two last are java package
        result.setPackageName(Joiner.on(".").join(chunks.subList(0, chunks.size() - 2)));


        return result;
    }

    /**
     * Generates valid javadoc for generated java method.
     *
     * @param soyFileName Soy fileName which will be included on javadoc.
     * @return Valid JavaDoc.
     */
    public static String generateJavaDocContent(String soyFileName) {
        return javaDocStart + soyFileName + " file.\n" + javaDocEnd;
    }

    /**
     * Generates path for parent directories based on package name (which is based on soy file's namespace).
     * Take in mind, it analyze structure of the target directory and adds missing part.
     * <p/>
     * Example:
     * Say, target directory is src/main/java/com/sitename
     * And package name is com.sitename.template.blocks
     * Output will be "template/blocks/"
     * i.e. it finds that target directory structure partly equals to
     * package names structure and adds missing part so that generated java classes won't be misplaced.
     * <p/>
     * Another example:
     * Say, target directory is src/main/java/generated
     * And package name is com.sitename.template.blocks
     * Output will be "com/sitename/template/blocks/"
     * Again, it finds that there is nothing similar on target directory path and
     * package names so it builds package structure from scratch.  
     * @param outputDirectoryChunks Chunks of target directory absolute path.
     * Looks like ("home", "username", "projects", "projectName", "src", "main", "java", "com", "sitename", "web", "template")
     * @param packageNameChunks Package name chunks, looks like ("com", "sitename", "web", "template").
     * @return directories Path between target directory and generated java class to safe it's package validity. 
     */
    public static String generateDirsPathForFileByPackage(ImmutableList<String> outputDirectoryChunks, ImmutableList<String> packageNameChunks) {
        String lastChunkOfOutputDirectoryPath = outputDirectoryChunks.get(outputDirectoryChunks.size() - 1);

        // test last chunk of directory path against package name chunks, from first to last
        for (String chunk : packageNameChunks) {
            if (lastChunkOfOutputDirectoryPath.equals(chunk)) {

                if (packageNameChunks.indexOf(chunk) == 0) {
                    // if chunk is first part
                    // then the rest of the package name should be prepended to Java class path.
                    return generateDirectoryPathFromStringChunks(packageNameChunks.subList(1, packageNameChunks.size()));
                } else {
                    // if not the first chunk is equal, than test if previous chunks of package name equals
                    // to second-to-last chunks of output directory path
                    // i.e. if directory path is ("foo", "bar", "template") and package name is ("bar", "template", "blocks")
                    // return "blocks/" path.
                    if (isEndOfFirstCollectionEqualEndOfSecondCollection(outputDirectoryChunks.subList(0, outputDirectoryChunks.size() - 1),
                            packageNameChunks.subList(0, packageNameChunks.indexOf(chunk)))) {

                        return generateDirectoryPathFromStringChunks(packageNameChunks.subList(packageNameChunks.indexOf(chunk) + 1,
                                packageNameChunks.size()));
                    }
                }
            }
        }

        return generateDirectoryPathFromStringChunks(packageNameChunks);
    }

    /**
     * Generates valid directory path from string chunks.
     * Separator char is taken from {@link java.io.File#separator}.
     * Example:
     * "one", "two", "three" --> one/two/three/
     * "foo" --> foo/
     *
     * @param chunks Collection of string chunks.
     * @return Valid directory path.
     */
    public static String generateDirectoryPathFromStringChunks(ImmutableList<String> chunks) {
        if (chunks.size() == 0) {
            return "";
        } else {
            return Joiner.on(File.separator).join(chunks) + File.separator;
        }
    }

    /**
     * Compares end of the first collection with second collection.
     * First collection must be bigger or equal in size with second collection.
     * <p>
     * Example:
     * ("one", "two", "three", "four") vs ("three", "four") returns true.
     * ("one", "two", "three", "four") vs ("one", "two") returns false.
     * </p>
     *
     * @param first  First collection.
     * @param second Second collection.
     * @return See method description above.
     */
    public static Boolean isEndOfFirstCollectionEqualEndOfSecondCollection(ImmutableList<String> first, ImmutableList<String> second) {
        // if second collection bigger than first, return false cause we can't compare them
        if (second.size() > first.size()) return false;

        ImmutableList<String> testable = first.subList(first.size() - second.size(), first.size());

        return testable.equals(second);
    }

    /**
     * Adds {@link java.io.File#separator} to the end of the provided string if there is no one.
     *
     * @param path Directory path.
     * @return Path with trailing {@link java.io.File#separator}.
     */
    public static String addTrailingFileSeparatorToPathIfNone(String path) {
        return path.endsWith(File.separator) ? path : path + File.separator;
    }

    /**
     * Checks if provided argument is a valid java package name (i.e. something like "com.companyname.projectname").
     * <p/>
     * In fact, it's really bad solution, because java <a href="http://java.sun.com/docs/books/jls/second_edition/html/lexical.doc.html#40625">allows</a>
     * almost any unicode symbol at the package name.
     * Second, validation algorithm really sucks. Well, I'll return to it later.
     *
     * @param packageName Java package name.
     * @return true if provided package name is valid, false otherwise.
     */
    public static Boolean isValidPackageName(String packageName) {
        // TODO: improve validation algorithm.
        ImmutableList<String> chunks = ImmutableList.copyOf(Splitter.on(".").trimResults().split(packageName));
        Pattern pattern = Pattern.compile("^[a-z_][a-zA-Z0-9_]+$");
        for (String chunk : chunks) {
            if (chunk.length() == 0) {
                return false;
            } else if (listOfReservedWords.contains(chunk)) {
                return false;
            } else {
                Matcher matcher = pattern.matcher(chunk);
                if (!matcher.find()) {
                    return false;
                }
            }
        }

        return true;
    }
}
